<?php

/**
 * The OpenID and Yadis discovery implementation for OpenID 1.2.
 */

// Do not allow direct access
defined( '_JEXEC' ) or die( 'Restricted access' );

require_once "Auth/OpenID.php";
require_once "Auth/OpenID/Parse.php";
require_once "Auth/OpenID/Message.php";
require_once "Auth/Yadis/XRIRes.php";
require_once "Auth/Yadis/Yadis.php";

// XML namespace value
define('Auth_OpenID_XMLNS_1_0', 'http://openid.net/xmlns/1.0');

// Yadis service types
define('Auth_OpenID_TYPE_1_2', 'http://openid.net/signon/1.2');
define('Auth_OpenID_TYPE_1_1', 'http://openid.net/signon/1.1');
define('Auth_OpenID_TYPE_1_0', 'http://openid.net/signon/1.0');
define('Auth_OpenID_TYPE_2_0_IDP', 'http://specs.openid.net/auth/2.0/server');
define('Auth_OpenID_TYPE_2_0', 'http://specs.openid.net/auth/2.0/signon');
define('Auth_OpenID_RP_RETURN_TO_URL_TYPE',
	   'http://specs.openid.net/auth/2.0/return_to');

function Auth_OpenID_getOpenIDTypeURIs()
{
	return array(Auth_OpenID_TYPE_2_0_IDP,
				 Auth_OpenID_TYPE_2_0,
				 Auth_OpenID_TYPE_1_2,
				 Auth_OpenID_TYPE_1_1,
				 Auth_OpenID_TYPE_1_0,
				 Auth_OpenID_RP_RETURN_TO_URL_TYPE);
}

/**
 * Object representing an OpenID service endpoint.
 */
class Auth_OpenID_ServiceEndpoint {
	function Auth_OpenID_ServiceEndpoint()
	{
		$this->claimed_id = null;
		$this->server_url = null;
		$this->type_uris = array();
		$this->local_id = null;
		$this->canonicalID = null;
		$this->used_yadis = false; // whether this came from an XRDS
		$this->display_identifier = null;
	}

	function getDisplayIdentifier()
	{
		if ($this->display_identifier) {
			return $this->display_identifier;
		}
		if (! $this->claimed_id) {
		  return $this->claimed_id;
		}
		$parsed = parse_url($this->claimed_id);
		$scheme = $parsed['scheme'];
		$host = $parsed['host'];
		$path = $parsed['path'];
		if (array_key_exists('query', $parsed)) {
			$query = $parsed['query'];
			$no_frag = "$scheme://$host$path?$query";
		} else {
			$no_frag = "$scheme://$host$path";
		}
		return $no_frag;
	}

	function usesExtension($extension_uri)
	{
		return in_array($extension_uri, $this->type_uris);
	}

	function preferredNamespace()
	{
		if (in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris) ||
			in_array(Auth_OpenID_TYPE_2_0, $this->type_uris)) {
			return Auth_OpenID_OPENID2_NS;
		} else {
			return Auth_OpenID_OPENID1_NS;
		}
	}

	/*
	 * Query this endpoint to see if it has any of the given type
	 * URIs. This is useful for implementing other endpoint classes
	 * that e.g. need to check for the presence of multiple versions
	 * of a single protocol.
	 *
	 * @param $type_uris The URIs that you wish to check
	 *
	 * @return all types that are in both in type_uris and
	 * $this->type_uris
	 */
	function matchTypes($type_uris)
	{
		$result = array();
		foreach ($type_uris as $test_uri) {
			if ($this->supportsType($test_uri)) {
				$result[] = $test_uri;
			}
		}

		return $result;
	}

	function supportsType($type_uri)
	{
		// Does this endpoint support this type?
		return ((in_array($type_uri, $this->type_uris)) ||
				(($type_uri == Auth_OpenID_TYPE_2_0) &&
				 $this->isOPIdentifier()));
	}

	function compatibilityMode()
	{
		return $this->preferredNamespace() != Auth_OpenID_OPENID2_NS;
	}

	function isOPIdentifier()
	{
		return in_array(Auth_OpenID_TYPE_2_0_IDP, $this->type_uris);
	}

	function fromOPEndpointURL($op_endpoint_url)
	{
		// Construct an OP-Identifier OpenIDServiceEndpoint object for
		// a given OP Endpoint URL
		$obj = new Auth_OpenID_ServiceEndpoint();
		$obj->server_url = $op_endpoint_url;
		$obj->type_uris = array(Auth_OpenID_TYPE_2_0_IDP);
		return $obj;
	}

	function parseService($yadis_url, $uri, $type_uris, $service_element)
	{
		// Set the state of this object based on the contents of the
		// service element.  Return true if successful, false if not
		// (if findOPLocalIdentifier returns false).
		$this->type_uris = $type_uris;
		$this->server_url = $uri;
		$this->used_yadis = true;

		if (!$this->isOPIdentifier()) {
			$this->claimed_id = $yadis_url;
			$this->local_id = Auth_OpenID_findOPLocalIdentifier(
													$service_element,
													$this->type_uris);
			if ($this->local_id === false) {
				return false;
			}
		}

		return true;
	}

	function getLocalID()
	{
		// Return the identifier that should be sent as the
		// openid.identity_url parameter to the server.
		if ($this->local_id === null && $this->canonicalID === null) {
			return $this->claimed_id;
		} else {
			if ($this->local_id) {
				return $this->local_id;
			} else {
				return $this->canonicalID;
			}
		}
	}

	/*
	 * Parse the given document as XRDS looking for OpenID services.
	 *
	 * @return array of Auth_OpenID_ServiceEndpoint or null if the
	 * document cannot be parsed.
	 */
	function fromXRDS($uri, $xrds_text)
	{
		$xrds =& Auth_Yadis_XRDS::parseXRDS($xrds_text);

		if ($xrds) {
			$yadis_services =
			  $xrds->services(array('filter_MatchesAnyOpenIDType'));
			return Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services);
		}

		return null;
	}

	/*
	 * Create endpoints from a DiscoveryResult.
	 *
	 * @param discoveryResult Auth_Yadis_DiscoveryResult
	 * @return array of Auth_OpenID_ServiceEndpoint or null if
	 * endpoints cannot be created.
	 */
	function fromDiscoveryResult($discoveryResult)
	{
		if ($discoveryResult->isXRDS()) {
			return Auth_OpenID_ServiceEndpoint::fromXRDS(
									 $discoveryResult->normalized_uri,
									 $discoveryResult->response_text);
		} else {
			return Auth_OpenID_ServiceEndpoint::fromHTML(
									 $discoveryResult->normalized_uri,
									 $discoveryResult->response_text);
		}
	}

	function fromHTML($uri, $html)
	{
		$discovery_types = array(
								 array(Auth_OpenID_TYPE_2_0,
									   'openid2.provider', 'openid2.local_id'),
								 array(Auth_OpenID_TYPE_1_1,
									   'openid.server', 'openid.delegate')
								 );

		$services = array();

		foreach ($discovery_types as $triple) {
			list($type_uri, $server_rel, $delegate_rel) = $triple;

			$urls = Auth_OpenID_legacy_discover($html, $server_rel,
												$delegate_rel);

			if ($urls === false) {
				continue;
			}

			list($delegate_url, $server_url) = $urls;

			$service = new Auth_OpenID_ServiceEndpoint();
			$service->claimed_id = $uri;
			$service->local_id = $delegate_url;
			$service->server_url = $server_url;
			$service->type_uris = array($type_uri);

			$services[] = $service;
		}

		return $services;
	}

	function copy()
	{
		$x = new Auth_OpenID_ServiceEndpoint();

		$x->claimed_id = $this->claimed_id;
		$x->server_url = $this->server_url;
		$x->type_uris = $this->type_uris;
		$x->local_id = $this->local_id;
		$x->canonicalID = $this->canonicalID;
		$x->used_yadis = $this->used_yadis;

		return $x;
	}
}

function Auth_OpenID_findOPLocalIdentifier($service, $type_uris)
{
	// Extract a openid:Delegate value from a Yadis Service element.
	// If no delegate is found, returns null.  Returns false on
	// discovery failure (when multiple delegate/localID tags have
	// different values).

	$service->parser->registerNamespace('openid',
										Auth_OpenID_XMLNS_1_0);

	$service->parser->registerNamespace('xrd',
										Auth_Yadis_XMLNS_XRD_2_0);

	$parser =& $service->parser;

	$permitted_tags = array();

	if (in_array(Auth_OpenID_TYPE_1_1, $type_uris) ||
		in_array(Auth_OpenID_TYPE_1_0, $type_uris)) {
		$permitted_tags[] = 'openid:Delegate';
	}

	if (in_array(Auth_OpenID_TYPE_2_0, $type_uris)) {
		$permitted_tags[] = 'xrd:LocalID';
	}

	$local_id = null;

	foreach ($permitted_tags as $tag_name) {
		$tags = $service->getElements($tag_name);

		foreach ($tags as $tag) {
			$content = $parser->content($tag);

			if ($local_id === null) {
				$local_id = $content;
			} else if ($local_id != $content) {
				return false;
			}
		}
	}

	return $local_id;
}

function filter_MatchesAnyOpenIDType(&$service)
{
	$uris = $service->getTypes();

	foreach ($uris as $uri) {
		if (in_array($uri, Auth_OpenID_getOpenIDTypeURIs())) {
			return true;
		}
	}

	return false;
}

function Auth_OpenID_bestMatchingService($service, $preferred_types)
{
	// Return the index of the first matching type, or something
	// higher if no type matches.
	//
	// This provides an ordering in which service elements that
	// contain a type that comes earlier in the preferred types list
	// come before service elements that come later. If a service
	// element has more than one type, the most preferred one wins.

	foreach ($preferred_types as $index => $typ) {
		if (in_array($typ, $service->type_uris)) {
			return $index;
		}
	}

	return count($preferred_types);
}

function Auth_OpenID_arrangeByType($service_list, $preferred_types)
{
	// Rearrange service_list in a new list so services are ordered by
	// types listed in preferred_types.  Return the new list.

	// Build a list with the service elements in tuples whose
	// comparison will prefer the one with the best matching service
	$prio_services = array();
	foreach ($service_list as $index => $service) {
		$prio_services[] = array(Auth_OpenID_bestMatchingService($service,
														$preferred_types),
								 $index, $service);
	}

	sort($prio_services);

	// Now that the services are sorted by priority, remove the sort
	// keys from the list.
	foreach ($prio_services as $index => $s) {
		$prio_services[$index] = $prio_services[$index][2];
	}

	return $prio_services;
}

// Extract OP Identifier services.  If none found, return the rest,
// sorted with most preferred first according to
// OpenIDServiceEndpoint.openid_type_uris.
//
// openid_services is a list of OpenIDServiceEndpoint objects.
//
// Returns a list of OpenIDServiceEndpoint objects."""
function Auth_OpenID_getOPOrUserServices($openid_services)
{
	$op_services = Auth_OpenID_arrangeByType($openid_services,
									 array(Auth_OpenID_TYPE_2_0_IDP));

	$openid_services = Auth_OpenID_arrangeByType($openid_services,
									 Auth_OpenID_getOpenIDTypeURIs());

	if ($op_services) {
		return $op_services;
	} else {
		return $openid_services;
	}
}

function Auth_OpenID_makeOpenIDEndpoints($uri, $yadis_services)
{
	$s = array();

	if (!$yadis_services) {
		return $s;
	}

	foreach ($yadis_services as $service) {
		$type_uris = $service->getTypes();
		$uris = $service->getURIs();

		// If any Type URIs match and there is an endpoint URI
		// specified, then this is an OpenID endpoint
		if ($type_uris &&
			$uris) {
			foreach ($uris as $service_uri) {
				$openid_endpoint = new Auth_OpenID_ServiceEndpoint();
				if ($openid_endpoint->parseService($uri,
												   $service_uri,
												   $type_uris,
												   $service)) {
					$s[] = $openid_endpoint;
				}
			}
		}
	}

	return $s;
}

function Auth_OpenID_discoverWithYadis($uri, &$fetcher,
			  $endpoint_filter='Auth_OpenID_getOPOrUserServices',
			  $discover_function=null)
{
	// Discover OpenID services for a URI. Tries Yadis and falls back
	// on old-style <link rel='...'> discovery if Yadis fails.

	// Might raise a yadis.discover.DiscoveryFailure if no document
	// came back for that URI at all.  I don't think falling back to
	// OpenID 1.0 discovery on the same URL will help, so don't bother
	// to catch it.
	if ($discover_function === null) {
		$discover_function = array('Auth_Yadis_Yadis', 'discover');
	}

	$openid_services = array();

	$response = call_user_func_array($discover_function,
									 array($uri, &$fetcher));

	$yadis_url = $response->normalized_uri;
	$yadis_services = array();

	if ($response->isFailure()) {
		return array($uri, array());
	}

	$openid_services = Auth_OpenID_ServiceEndpoint::fromXRDS(
										 $yadis_url,
										 $response->response_text);

	if (!$openid_services) {
		if ($response->isXRDS()) {
			return Auth_OpenID_discoverWithoutYadis($uri,
													$fetcher);
		}

		// Try to parse the response as HTML to get OpenID 1.0/1.1
		// <link rel="...">
		$openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
										$yadis_url,
										$response->response_text);
	}

	$openid_services = call_user_func_array($endpoint_filter,
											array(&$openid_services));

	return array($yadis_url, $openid_services);
}

function Auth_OpenID_discoverURI($uri, &$fetcher)
{
	$uri = Auth_OpenID::normalizeUrl($uri);
	return Auth_OpenID_discoverWithYadis($uri, $fetcher);
}

function Auth_OpenID_discoverWithoutYadis($uri, &$fetcher)
{
	$http_resp = @$fetcher->get($uri);

	if ($http_resp->status != 200 and $http_resp->status != 206) {
		return array($uri, array());
	}

	$identity_url = $http_resp->final_url;

	// Try to parse the response as HTML to get OpenID 1.0/1.1 <link
	// rel="...">
	$openid_services = Auth_OpenID_ServiceEndpoint::fromHTML(
										   $identity_url,
										   $http_resp->body);

	return array($identity_url, $openid_services);
}

function Auth_OpenID_discoverXRI($iname, &$fetcher)
{
	$resolver = new Auth_Yadis_ProxyResolver($fetcher);
	list($canonicalID, $yadis_services) =
		$resolver->query($iname,
						 Auth_OpenID_getOpenIDTypeURIs(),
						 array('filter_MatchesAnyOpenIDType'));

	$openid_services = Auth_OpenID_makeOpenIDEndpoints($iname,
													   $yadis_services);

	$openid_services = Auth_OpenID_getOPOrUserServices($openid_services);

	for ($i = 0; $i < count($openid_services); $i++) {
		$openid_services[$i]->canonicalID = $canonicalID;
		$openid_services[$i]->claimed_id = $canonicalID;
		$openid_services[$i]->display_identifier = $iname;
	}

	// FIXME: returned xri should probably be in some normal form
	return array($iname, $openid_services);
}

function Auth_OpenID_discover($uri, &$fetcher)
{
	// If the fetcher (i.e., PHP) doesn't support SSL, we can't do
	// discovery on an HTTPS URL.
	if ($fetcher->isHTTPS($uri) && !$fetcher->supportsSSL()) {
		return array($uri, array());
	}

	if (Auth_Yadis_identifierScheme($uri) == 'XRI') {
		$result = Auth_OpenID_discoverXRI($uri, $fetcher);
	} else {
		$result = Auth_OpenID_discoverURI($uri, $fetcher);
	}

	// If the fetcher doesn't support SSL, we can't interact with
	// HTTPS server URLs; remove those endpoints from the list.
	if (!$fetcher->supportsSSL()) {
		$http_endpoints = array();
		list($new_uri, $endpoints) = $result;

		foreach ($endpoints as $e) {
			if (!$fetcher->isHTTPS($e->server_url)) {
				$http_endpoints[] = $e;
			}
		}

		$result = array($new_uri, $http_endpoints);
	}

	return $result;
}

?>
